package com.szh.gulimall.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.szh.gulimall.product.service.CategoryBrandRelationService;
import com.szh.gulimall.product.vo.Catelog2Vo;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.szh.common.utils.PageUtils;
import com.szh.common.utils.Query;

import com.szh.gulimall.product.dao.CategoryDao;
import com.szh.gulimall.product.entity.CategoryEntity;
import com.szh.gulimall.product.service.CategoryService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    private static final Logger LOGGER = LoggerFactory.getLogger(CategoryServiceImpl.class);

    @Autowired
    private CategoryBrandRelationService categoryBrandRelationService;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private RedissonClient redisson;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<>()
        );
        return new PageUtils(page);
    }

    /**
     * 查询所有分类及子分类，以树形结构组装起来
     */
    @Override
    public List<CategoryEntity> listWithTree() {
        //1.查询出所有分类
        List<CategoryEntity> categoryEntityList = baseMapper.selectList(null);
        //2.组装成父子树形结构，先查询一级分类
        List<CategoryEntity> resultList = categoryEntityList.stream()
                .filter(categoryEntity -> categoryEntity.getParentCid() == 0)
                .map(categoryEntity -> {
                    categoryEntity.setChildren(getCategoryChildren(categoryEntity, categoryEntityList));
                    return categoryEntity;
                })
                .sorted(Comparator.comparingInt(category -> (Objects.isNull(category.getSort()) ? 0 : category.getSort())))
                .collect(Collectors.toList());
        return resultList;
    }

    private List<CategoryEntity> getCategoryChildren(CategoryEntity root, List<CategoryEntity> categoryEntityList) {
        List<CategoryEntity> childrenList = categoryEntityList.stream()
                .filter(category -> category.getParentCid() == root.getCatId())
                .map(category -> {
                    category.setChildren(getCategoryChildren(category, categoryEntityList));
                    return category;
                })
                .sorted(Comparator.comparingInt(category -> (Objects.isNull(category.getSort()) ? 0 : category.getSort())))
                .collect(Collectors.toList());
        return childrenList;
    }

    /**
     * 批量逻辑删除
     */
    @Transactional
    @Override
    public void removeCategoryByIds(List<Long> catIds) {
        //TODO 检查当前要删除的菜单是否被其他地方引用
        baseMapper.deleteBatchIds(catIds);
    }

    /**
     * 找到categoryId的完整路径 [父/子/孙]
     */
    @Override
    public Long[] queryCatelogPath(Long categoryId) {
        List<Long> pathList = new ArrayList<>();
        //在如下递归方法获取的list集合：225的父节点34，34的父节点2，[225, 34, 2]，对list做反转
        queryCatelogParentPath(categoryId, pathList);
        //最终list：[2, 34, 225]
        Collections.reverse(pathList);
        return pathList.toArray(new Long[pathList.size()]);
    }

    private void queryCatelogParentPath(Long categoryId, List<Long> pathList) {
        //将当前节点id加入路径集合
        pathList.add(categoryId);
        CategoryEntity categoryEntity = this.getById(categoryId);
        //如果当前节点存在父节点，则递归查找
        if (categoryEntity.getParentCid() != 0) {
            queryCatelogParentPath(categoryEntity.getParentCid(), pathList);
        }
    }

    //---------------- 如下全是SpringCache 结合 Redis的相关业务代码 ----------------

    /**
     * 采用缓存失效模式@CacheEvict，当修改分类菜单数据之后，就删除之前在Redis中缓存的所有分类数据
     * 缓存所有分类数据对应的方法是 getLevel1Categorys 以及 getCatalogJsonWithSpringCache，对应按照它的key删除即可
     * 当下次请求这两个方法时，会重新查询数据库，并将我们修改之后的菜单数据存入Redis
     *
     * @CacheEvict：按照cacheNames缓存分区以及key值删除指定方法对应的缓存，也可以删除cacheNames缓存分区下的所有缓存数据
     * @Caching：批量执行 @CacheEvict、@CachePut、@Cacheable
     */
    //@CacheEvict(cacheNames = {"category"}, key = "'getLevel1Categorys'")
    //@CacheEvict(cacheNames = {"category"}, allEntries = true) 等价于下面的@Caching
    @Caching(evict = {
            @CacheEvict(cacheNames = {"category"}, key = "'getLevel1Categorys'"),
            @CacheEvict(cacheNames = {"category"}, key = "'getCatalogJsonWithSpringCache'")
    })
    @Transactional
    @Override
    public void updateDetail(CategoryEntity category) {
        //首先更新分类表自己的数据
        this.updateById(category);
        if (!StringUtils.isEmpty(category.getName())) {
            //同步更新分类-品牌关联表中的数据
            categoryBrandRelationService.updateCategoryInfo(category.getCatId(), category.getName());
        }
    }

    /**
     * 查询所有的一级分类
     * @Cacheable：代表当前方法的返回结果需要缓存
     * 如果缓存中有，则方法不会被调用，直接返回；如果缓存中没有，则会调用方法获取结果再放入缓存
     * 每一个需要缓存的数据都要指定一个名字（分区），这里按照商城业务类型区分
     */
    @Cacheable(cacheNames = {"category"}, key = "#root.methodName", sync = true)
    @Override
    public List<CategoryEntity> getLevel1Categorys() {
        System.out.println(Thread.currentThread().getName() + " 调用了 getLevel1Categorys 方法....");
        QueryWrapper<CategoryEntity> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("parent_cid", 0);
        List<CategoryEntity> categoryEntityList = baseMapper.selectList(queryWrapper);
        return categoryEntityList;
    }

    /**
     * 1、缓存穿透：针对空结果进行缓存
     *       解决：使用 SpringCache，yml中配置 spring.cache.redis.cache-null-values = true
     * 2、缓存击穿：加锁，保证同一时刻只有一个请求进来查询并将结果存入Redis，
     *            其他请求需等待最早的请求结束之后，再查Redis，如果有直接返回，不再走数据库
     *       解决：在 @Cacheable 注解中添加 sync = true
     * 3、缓存雪崩：针对多个key设置不同的过期时间，避免在同一时间大面积的key同时失效
     *       解决：使用 SpringCache，yml中配置 spring.cache.redis.time-to-live = xxx
     */
    @Cacheable(cacheNames = {"category"}, key = "#root.method.name", sync = true)
    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJsonWithSpringCache() {
        System.out.println(Thread.currentThread().getName() + "这里查询了数据库....");
        //将数据库的多次查询变为一次
        List<CategoryEntity> categoryEntityList = baseMapper.selectList(null);
        //查询所有的一级分类
        List<CategoryEntity> level1CategoryList = getParentCidCategoryList(categoryEntityList, 0L);
        //封装数据
        Map<String, List<Catelog2Vo>> map = level1CategoryList.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            //查询每个一级分类下的所有二级分类
            List<CategoryEntity> categoryEntities = getParentCidCategoryList(categoryEntityList, v.getCatId());
            List<Catelog2Vo> catelog2VoList = null;
            if (!CollectionUtils.isEmpty(categoryEntities)) {
                catelog2VoList = categoryEntities.stream().map(level2 -> {
                    Catelog2Vo catelog2Vo = new Catelog2Vo(
                            v.getCatId().toString(), null, level2.getCatId().toString(), level2.getName());
                    //查询每个二级分类下的所有三级分类
                    List<CategoryEntity> list = getParentCidCategoryList(categoryEntityList, level2.getCatId());
                    if (!CollectionUtils.isEmpty(list)) {
                        List<Catelog2Vo.Category3Vo> catalog3List = list.stream().map(level3 -> {
                            Catelog2Vo.Category3Vo category3Vo = new Catelog2Vo.Category3Vo(
                                    level2.getCatId().toString(), level3.getCatId().toString(), level3.getName());
                            return category3Vo;
                        }).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(catalog3List);
                    }
                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return catelog2VoList;
        }));
        return map;
    }

    //---------------- 如下全是RedisTemplate结合Redisson的相关业务代码 ----------------

    /**
     * 加载网站首页的所有菜单数据（这里用到了Redis的缓存、分布式锁Redisson）
     * 将电商网站首页的所有菜单数据查询出来之后，存入Redis
     * 下次查询如果Redis中存在缓存，则直接到Redis中获取，不再走MySQL
     */
    //@Override
    public Map<String, List<Catelog2Vo>> getCatalogJsonWithRedis() {
        String catalogJson = redisTemplate.opsForValue().get("catalogJson");
        //如果Redis中没有，则调用getCatalogJson去数据库中查一次
        if (StringUtils.isEmpty(catalogJson)) {
            System.out.println(Thread.currentThread().getName() + "缓存不命中，查询数据库....");
            //getCatalogJsonWithRedisLockVersion2方法中需要加锁
            Map<String, List<Catelog2Vo>> data = getCatalogJsonWithRedisLockVersion3();
            return data; //将数据返回
        }
        System.out.println(Thread.currentThread().getName() + "缓存命中，直接返回....");
        //如果Redis中有，则转为我们指定的对象，返回数据
        Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJson,
                new TypeReference<Map<String, List<Catelog2Vo>>>(){});
        return result;
    }

    /**
     * version3：通过Redisson实现加锁、解锁的原子操作
     */
    public Map<String, List<Catelog2Vo>> getCatalogJsonWithRedisLockVersion3() {
        RLock lock = redisson.getLock("catalogJson-lock");
        lock.lock(); //加锁
        System.out.println(Thread.currentThread().getName() + "获取分布式锁成功....");
        Map<String, List<Catelog2Vo>> data;
        try {
            data = getDataWithDbRedis(); //从MySQL或Redis中获取数据
        } finally {
            lock.unlock(); //解锁
        }
        return data;
    }

    /**
     * version2：加锁完成了原子操作，加锁同时，设置过期时间，设置锁的唯一uuid
     *          借助Lua脚本实现删锁的原子操作
     */
    public Map<String, List<Catelog2Vo>> getCatalogJsonWithRedisLockVersion2() {
        //通过Redis命令实现分布式锁，占有锁的同时，指定uuid，确保不要删除其他线程的锁，同时设置锁的过期时间
        String uuid = UUID.randomUUID().toString();
        Boolean lockSuccess = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
        if (lockSuccess) { //加锁成功
            System.out.println(Thread.currentThread().getName() + "获取分布式锁成功....");
            Map<String, List<Catelog2Vo>> data;
            try {
                data = getDataWithDbRedis(); //从MySQL或Redis中获取数据
            } finally {
                //Lua脚本进行锁删除，删除之前，先从Redis中获取锁的值进行uuid比对，看看是不是当前线程自己的锁，是了话再删
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
                        Arrays.asList("lock"), uuid);
            }
            return data;
        } else { //加锁失败，调用自己再次尝试加锁，相当于自旋
            System.out.println(Thread.currentThread().getName() + "获取分布式锁失败....");
            try {
                Thread.sleep(300);
            } catch (InterruptedException ex) {
                LOGGER.error("异常信息：{}", ex.getMessage());
            }
            return getCatalogJsonWithRedisLockVersion2();
        }
    }

    /**
     * version1：加锁完成了原子操作，加锁同时，设置过期时间，设置锁的唯一uuid
     *     缺陷：删锁无法做到原子操作
     *     完善：参考 getCatalogJsonWithRedisLockVersion2 方法，采用Lua脚本实现删锁的原子操作
     */
    public Map<String, List<Catelog2Vo>> getCatalogJsonWithRedisLockVersion1() {
        //通过Redis命令实现分布式锁，占有锁的同时，指定uuid，确保不要删除其他线程的锁，同时设置锁的过期时间
        String uuid = UUID.randomUUID().toString();
        Boolean lockSuccess = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
        if (lockSuccess) { //加锁成功
            Map<String, List<Catelog2Vo>> data = getDataWithDbRedis();
            //删除之前，先从Redis中获取锁进行比对，看看是不是当前线程自己的锁，是了话再删
            String lockValue = redisTemplate.opsForValue().get("lock");
            if (uuid.equals(lockValue)) {
                redisTemplate.delete("lock");
            }
            return data;
        } else { //加锁失败，调用自己再次尝试加锁，相当于自旋
            try {
                Thread.sleep(300);
            } catch (InterruptedException ex) {
                LOGGER.error("异常信息：{}", ex.getMessage());
            }
            return getCatalogJsonWithRedisLockVersion1();
        }
    }

    /**
     * 从MySQL或Redis中获取菜单数据
     * 如果Redis缓存中存在，则直接返回；如果不存在，则去数据库中查一遍，再存入Redis，最后返回数据
     */
    private Map<String, List<Catelog2Vo>> getDataWithDbRedis() {
        //解决缓存击穿：加锁之后，第一次请求结束之后，后续请求进来之后首先再查一次Redis，如果有，就直接返回
        String catalogJson = redisTemplate.opsForValue().get("catalogJson");
        //如果Redis中有，则直接返回数据
        if (!StringUtils.isEmpty(catalogJson)) {
            Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJson,
                    new TypeReference<Map<String, List<Catelog2Vo>>>(){});
            return result;
        }
        //如果Redis中没有，则执行下面的代码去数据库中查菜单数据，查询完毕再存入Redis
        System.out.println(Thread.currentThread().getName() + "这里查询了数据库....");
        //将数据库的多次查询变为一次
        List<CategoryEntity> categoryEntityList = baseMapper.selectList(null);
        //查询所有的一级分类
        List<CategoryEntity> level1CategoryList = getParentCidCategoryList(categoryEntityList, 0L);
        //封装数据
        Map<String, List<Catelog2Vo>> map = level1CategoryList.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            //查询每个一级分类下的所有二级分类
            List<CategoryEntity> categoryEntities = getParentCidCategoryList(categoryEntityList, v.getCatId());
            List<Catelog2Vo> catelog2VoList = null;
            if (!CollectionUtils.isEmpty(categoryEntities)) {
                catelog2VoList = categoryEntities.stream().map(level2 -> {
                    Catelog2Vo catelog2Vo = new Catelog2Vo(
                            v.getCatId().toString(), null, level2.getCatId().toString(), level2.getName());
                    //查询每个二级分类下的所有三级分类
                    List<CategoryEntity> list = getParentCidCategoryList(categoryEntityList, level2.getCatId());
                    if (!CollectionUtils.isEmpty(list)) {
                        List<Catelog2Vo.Category3Vo> catalog3List = list.stream().map(level3 -> {
                            Catelog2Vo.Category3Vo category3Vo = new Catelog2Vo.Category3Vo(
                                    level2.getCatId().toString(), level3.getCatId().toString(), level3.getName());
                            return category3Vo;
                        }).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(catalog3List);
                    }
                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return catelog2VoList;
        }));
        //将查询到的菜单数据转为json再存入Redis中
        String json = JSON.toJSONString(map);
        redisTemplate.opsForValue().set("catalogJson", json);
        return map;
    }

    /**
     * 在所有分类列表中查询指定parentCid分类下的所有子分类列表
     * @param categoryEntityList 所有分类列表
     * @param parentCid 指定parentCid
     * @return 指定parentCid分类下的所有子分类列表
     */
    private List<CategoryEntity> getParentCidCategoryList(List<CategoryEntity> categoryEntityList, Long parentCid) {
        return categoryEntityList.stream()
                .filter(item -> item.getParentCid().equals(parentCid))
                .collect(Collectors.toList());
    }
}